定时任务:完成容器数据库备份与滚动删除
SSH 模块开发完成后,我们可以回到定时任务这一核心需求:通过 SSH 远程连接宿主机,执行 mongodump 备份容器中的 MongoDB 数据库,然后将备份文件从容器拷贝到宿主机,最后定期清理超过指定天数的历史备份目录。
备份流程设计
整个备份流程包含三个阶段:
- 容器内备份 -- 通过
docker exec在容器中执行mongodump命令,将数据库备份到容器内临时目录 - 拷贝至宿主机 -- 通过
docker cp将容器内的备份文件拷贝到宿主机指定目录 - 滚动删除 -- 使用
find命令删除超过指定天数的历史备份目录
MongoDB Container
|
| mongodump --uri=... --out=/tmp/logs/{timestamp}
v
Container /tmp/logs/
|
| docker cp {container}:/tmp/logs/{timestamp} /srv/logs/{timestamp}
v
Host /srv/logs/
|
| find /srv/logs -type d -mtime +7 -exec rm -rf {} \;
v
Retained backups (last 7 days)
text
定义备份命令变量
在 TaskService 中定义备份所需的关键变量和命令:
// task.service.ts
import { Injectable } from '@nestjs/common';
import { SshService } from '../common/utils/ssh/ssh.service';
@Injectable()
export class TaskService {
// Configurable parameters
private containerName = 'mongo-container';
private uri = 'mongodb://root:123456@localhost:27017/messages?authSource=admin';
private hostBackupPath = '/srv/logs';
constructor(private sshService: SshService) {}
async backupDatabase() {
const now = new Date();
const timestamp = now.getTime();
const collectionName = 'log';
// Step 1: mongodump inside container
const cmd = `docker exec -i \${containerName} mongodump --uri=\${uri} ` +
`--collection=\${collectionName} ` +
`--out=/tmp/logs/log-${timestamp}`;
// Step 2: copy backup to host
const cpCmd = `docker cp \${containerName}:/tmp/logs/log-${timestamp} \${hostBackupPath}`;
// Execute backup commands
const res = await this.sshService.exec(`${cmd} && ${cpCmd}`);
// Verify backup
const resLs = await this.sshService.exec(`ls -la ${this.hostBackupPath}`);
console.log('Backup result:', res);
console.log('Backup directory:', resLs);
}
}
typescript
注意事项:
docker exec使用-i参数而非-it。-t会分配伪终端,在 SSH 非交互式执行时会报input device is not TTY错误- 命令中
${variableName}的变量替换由 Shell 在远程服务器上执行
在 Conditional 模块中注册 SSH
在 conditional.module.ts 中导入 SSH 模块,配置实际的服务器连接信息:
// conditional.module.ts
import { SshModule } from './utils/ssh/ssh.module';
@Module({
imports: [
SshModule.forRoot({
host: '192.168.3.7',
port: 22,
username: 'root',
password: '***',
}),
],
// ...
})
export class ConditionalModule {}
typescript
Docker exec 中 -t 参数的问题
在远程执行 docker exec -it 时,SSH 客户端没有真正的终端界面(TTY),因此 -t 参数会导致以下错误:
the input device is not a TTY
text
解决方案是将 -it 改为仅使用 -i:
-i(--interactive):保持 STDIN 打开,即使没有附加终端-t(--tty):分配伪终端,仅在交互式场景使用
对于定时任务这种非交互式场景,只保留 -i 即可。
实现滚动删除逻辑
使用 Linux 的 find 命令删除超过指定时间的历史备份目录:
async deleteOldBackups() {
// Delete directories older than 7 days
const deleteCmd = `find ${this.hostBackupPath} -type d -mtime +7 ` +
`-exec rm -rf {} \\;`;
await this.sshService.exec(deleteCmd);
// Verify remaining backups
const resLs = await this.sshService.exec(`ls -la ${this.hostBackupPath}`);
console.log('Remaining backups:', resLs);
}
typescript
关键参数说明:
| 参数 | 含义 |
|---|---|
-type d | 只匹配目录 |
-mtime +7 | 修改时间超过 7 天 |
-mmin +1 | 修改时间超过 1 分钟(用于测试) |
-exec rm -rf {} \; | 对匹配结果执行强制删除 |
\; | -exec 命令的终止符(必须转义) |
转义注意事项: 在 JavaScript 字符串中,find 命令末尾的 \; 需要写成 \\;,否则反斜杠会被 JS 引擎解释为转义符。
完整的定时备份任务
将备份和删除逻辑组合为一个定时任务:
import { Cron } from '@nestjs/schedule';
@Injectable()
export class LogDbCronService {
private containerName = 'mongo-container';
private uri = 'mongodb://root:123456@localhost:27017/messages?authSource=admin';
private hostBackupPath = '/srv/logs';
private collectionName = 'log';
constructor(private sshService: SshService) {}
@Cron('0 * * * *') // Every hour at minute 0
async handleBackupAndCleanup() {
const now = new Date();
const timestamp = now.getTime();
// Backup
const cmd = `docker exec -i ${this.containerName} mongodump ` +
`--uri=${this.uri} ` +
`--collection=${this.collectionName} ` +
`--out=/tmp/logs/log-${timestamp}`;
const cpCmd = `docker cp ${this.containerName}:/tmp/logs/log-${timestamp} ${this.hostBackupPath}`;
try {
await this.sshService.exec(`${cmd} && ${cpCmd}`);
} catch (error) {
console.error('Backup failed:', error.toString());
}
// Cleanup old backups (older than 7 days)
const deleteCmd = `find ${this.hostBackupPath} -type d -mtime +7 -exec rm -rf {} \\;`;
await this.sshService.exec(deleteCmd);
}
}
typescript
Cron 表达式说明
NestJS 的 @nestjs/schedule 模块使用 6 位 Cron 表达式(比 Linux 的 5 位多了一个"秒"字段):
┌───────── 秒 (0-59)
│ ┌───────── 分钟 (0-59)
│ │ ┌───────── 小时 (0-23)
│ │ │ ┌───────── 日 (1-31)
│ │ │ │ ┌───────── 月 (1-12)
│ │ │ │ │ ┌───────── 星期 (0-7, 0和7都代表周日)
│ │ │ │ │ │
* * * * * *
text
常用示例:
| 表达式 | 含义 |
|---|---|
0 * * * * * | 每分钟的第 0 秒 |
0 0 * * * * | 每小时的第 0 分 0 秒 |
0 0 3 * * * | 每天凌晨 3 点 |
0 0 0 * * 1 | 每周一零点 |
测试验证
- 在
.env中设置CRON_ON=true开启定时任务 - 准备测试数据:通过
docker cp将备份文件传入容器,再通过mongorestore恢复数据 - 调整 Cron 表达式为
0 * * * * *(每分钟执行一次)进行测试 - 在关键行设置断点,通过调试逐步验证
cmd和cpCmd变量的值是否正确 - 检查宿主机
/srv/logs/目录,确认备份文件已成功拷贝 - 使用
-mmin +1替代-mtime +7测试删除逻辑,确认超过 1 分钟的目录被正确清理
本节总结
- 实现了通过 SSH 远程执行
docker exec mongodump备份数据库的完整流程 - 使用
docker cp将容器内备份拷贝到宿主机持久化存储 - 通过
find -mtime +N -exec rm -rf {} \;实现历史备份的滚动删除 - 解决了
docker exec -it在非交互式 SSH 环境中的 TTY 错误 - 所有路径参数(
containerName、uri、hostBackupPath)均可通过环境变量配置
↑